package io.libp2p.example.chat;

import android.content.Context;
import org.webrtc.CandidatePairChangeEvent;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.RtpReceiver;
import org.webrtc.RtpTransceiver;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PeerConnectHelper {
    private static final String TAG = "PeerConnectHelper";
    private static final String VIDEO_TRACK_ID = "ebo_video_track";

    public interface DataCallback{
        void onDataRecv(String peerId, String data);
        void onDisConnect(PeerConnectHelper peerConnectHelper, boolean isCreateOffer);
        void onConnectChange(PeerConnectHelper peerConnectHelper);
    }

    Context context;
    PeerConnection peerConnection;
    DataChannel audioDataChannel;
    NatsHelper spdSocketClient;
    DataCallback dataCallback;
    List<IceCandidate> iceCandidateList = new ArrayList<>();
    boolean isStartP2p = false;

    boolean isCreateOffer;

    String peerId;

    boolean isP2P = false;

    SocketServer socketServer;

    Map<String, SocketConnect> connectMap = new HashMap<>();

    public PeerConnectHelper(Context context, String peerId, NatsHelper spdSocketClient, DataCallback dataCallback) {
        this.context = context;
        this.peerId = peerId;
        this.spdSocketClient = spdSocketClient;
        this.dataCallback = dataCallback;

        socketServer = new SocketServer(this);
    }

    public String getAddress(){
        return socketServer.getAddress();
    }

    public PeerConnection.PeerConnectionState getConnectionStatus() {
        if (peerConnection == null) {
            return PeerConnection.PeerConnectionState.NEW;
        } else {
            return peerConnection.connectionState();
        }
    }

    public PeerConnection.IceConnectionState getIceConnectionStatus() {
        if (peerConnection == null) {
            return PeerConnection.IceConnectionState.NEW;
        } else {
            return peerConnection.iceConnectionState();
        }
    }

    public boolean isConnect(){
        return getConnectionStatus() == PeerConnection.PeerConnectionState.CONNECTED;
    }

    private void createPeerConnection(IceServerJson server) {
        if (peerConnection == null) {
            List<PeerConnection.IceServer> iceServers = new ArrayList<>();

            for(String stun: StunText.stunList){
                iceServers.add(PeerConnection.IceServer.builder(stun).createIceServer());
            }

            if(server != null){
                iceServers.add(PeerConnection.IceServer.builder(server.server_uris).setUsername(server.username).setPassword(server.password).createIceServer());
            }

            //iceServers.add(PeerConnection.IceServer.builder("turn:115.190.89.224:3478").setUsername("voidtech").setPassword("5e1b5e6d").createIceServer());

            MyLog.E(TAG, "iceServers="+iceServers);

            PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions());

            PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
            PeerConnectionFactory factory = PeerConnectionFactory.builder().setOptions(options).createPeerConnectionFactory();
            PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(iceServers);
            peerConnection = factory.createPeerConnection(config, connectObserver);

            createDataChannel();
        }
    }

    private MediaConstraints createMediaConstraints(){
        MediaConstraints mediaConstraints = new MediaConstraints();
        mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));

//        mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googImprovedWifiBw", "true"));
        mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEnableIPv6", "false")); // 禁用 IPv6

        //mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
        //mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));

        return mediaConstraints;
    }

    public void startSetRemote(String sdp){
        isCreateOffer = true;

        HttpUtils.startGet(server -> setRemoteDescription(sdp, server));
    }

    public void setRemoteDescription(String sdp, IceServerJson serverJson) {
        MyLog.D(TAG, "setRemoteDescription Offer");

        createPeerConnection(serverJson);

        SessionDescription desc = new SessionDescription(SessionDescription.Type.OFFER, sdp);

        peerConnection.setRemoteDescription(new SdpObserver() {
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {

            }

            @Override
            public void onSetSuccess() {
                MyLog.D(TAG, "setRemoteDescription onSetSuccess");

                peerConnection.createAnswer(new SdpListener() {
                    @Override
                    public void onCreateSuccess(SessionDescription sessionDescription) {
                        MyLog.I(TAG,"createAnswer");

                        peerConnection.setLocalDescription(new SdpObserver() {
                            @Override
                            public void onCreateSuccess(SessionDescription sessionDescription) {
                                MyLog.I(TAG,"setLocalDescription onCreateSuccess");
                            }

                            @Override
                            public void onSetSuccess() {
                                MyLog.I(TAG,"setLocalDescription onSetSuccess");
                                String sdp = peerConnection.getLocalDescription().description;
                                SpdJson spdJson = new SpdJson(SpdJson.TYPE_ANSWER, peerId);
                                spdJson.sdp = sdp;
                                spdSocketClient.sendSpdJson(spdJson);
                            }

                            @Override
                            public void onCreateFailure(String s) {
                                MyLog.E(TAG,"setLocalDescription onCreateFailure:"+s);
                            }

                            @Override
                            public void onSetFailure(String s) {
                                MyLog.E(TAG,"setLocalDescription onSetFailure:"+s);
                            }
                        }, sessionDescription);
                    }
                }, createMediaConstraints());
            }

            @Override
            public void onCreateFailure(String s) {

            }

            @Override
            public void onSetFailure(String s) {

            }
        }, desc);
    }

    public void setAnswer(String sdp){
        SessionDescription desc = new SessionDescription(SessionDescription.Type.ANSWER, sdp);

        MyLog.D(TAG, "start setAnswer");

        spdSocketClient.sendSpdJson(new SpdJson(SpdJson.TYPE_START_P2P, peerId));

        peerConnection.setRemoteDescription(new SdpObserver() {
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {
                MyLog.D(TAG, "setAnswer onCreateSuccess");
            }

            @Override
            public void onSetSuccess() {
                MyLog.D(TAG, "setAnswer onSetSuccess");
                startP2PConnect();
            }

            @Override
            public void onCreateFailure(String s) {
                MyLog.E(TAG, "setAnswer onCreateFailure");
            }

            @Override
            public void onSetFailure(String s) {
                MyLog.E(TAG, "setAnswer onSetFailure");
            }
        }, desc);
    }

    public void startConnect() {
        HttpUtils.startGet(server -> {
            createPeerConnection(server);

            peerConnection.createOffer(new SdpListener() {
                @Override
                public void onCreateSuccess(SessionDescription sessionDescription) {
                    peerConnection.setLocalDescription(new SdpListener() {
                        @Override
                        public void onSetSuccess() {
                            String sdp = peerConnection.getLocalDescription().description;

                            SpdJson spdJson = new SpdJson(SpdJson.TYPE_OFFER, peerId);
                            spdJson.sdp = sdp;

                            spdSocketClient.sendSpdJson(spdJson);

                            MyLog.D(TAG, "Send Local SDP:" + sdp);
                        }
                    }, sessionDescription);
                }
            }, createMediaConstraints());
        });
    }

    public void stopConnect(Boolean needStop) {
        if (needStop) {
            release();
        }
    }

    public void release() {
        try {
            if (peerConnection != null) {
                peerConnection.close();
                peerConnection = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void createDataChannel() {
        if (audioDataChannel == null) {
            audioDataChannel = peerConnection.createDataChannel("DataChannel", getDataChannelInit(102));
            audioDataChannel.registerObserver(audioDataObserver);
        }
    }

    private DataChannel.Init getDataChannelInit(int id) {
        DataChannel.Init init = new DataChannel.Init();
        init.ordered = true;
        //init.negotiated = false;
        init.maxRetransmits = -1;
        init.maxRetransmitTimeMs = -1;

        init.id = id;
        //init.protocol = "";
        return init;
    }

    private final PeerConnection.Observer connectObserver = new PeerConnection.Observer() {

        @Override
        public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) {
            MyLog.E(TAG, "PairChanged local="+event.local.sdp);
            MyLog.E(TAG, "PairChanged remote="+event.remote.sdp);
            isP2P = !(event.local.sdp.contains("relay") || event.remote.sdp.contains("relay"));
            MyLog.E(TAG, "onSelectedCandidatePairChanged isP2P="+isP2P);
        }

        @Override
        public void onSignalingChange(PeerConnection.SignalingState state) {
            MyLog.E(TAG, "onSignalingChange state="+state);
        }

        @Override
        public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {

        }

        @Override
        public void onIceConnectionReceivingChange(boolean b) {
            MyLog.D(TAG, "onIceConnectionReceivingChange b=" + b);
        }

        @Override
        public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
            MyLog.D(TAG, "onIceGatheringChange iceGatheringState=" + iceGatheringState);
        }

        @Override
        public void onIceCandidate(IceCandidate iceCandidate) {
            MyLog.D(TAG, "onIceCandidate "+iceCandidate.sdp);

            SpdJson spdJson = new SpdJson(SpdJson.TYPE_CANDI, peerId);

            spdJson.sdp = iceCandidate.sdp;
            spdJson.sdpMid = iceCandidate.sdpMid;
            spdJson.sdpMLineIndex = iceCandidate.sdpMLineIndex;

            spdSocketClient.sendSpdJson(spdJson);
        }

        @Override
        public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
            for(IceCandidate iceCandidate: iceCandidates) {

                SpdJson spdJson = new SpdJson(SpdJson.TYPE_REMOVE_ICE, peerId);

                spdJson.sdp = iceCandidate.sdp;
                spdJson.sdpMid = iceCandidate.sdpMid;
                spdJson.sdpMLineIndex = iceCandidate.sdpMLineIndex;

                spdSocketClient.sendSpdJson(spdJson);
            }
        }

        @Override
        public void onAddStream(MediaStream mediaStream) {
            MyLog.E(TAG, "onAddStream");
        }

        @Override
        public void onRemoveStream(MediaStream mediaStream) {

        }

        @Override
        public void onDataChannel(DataChannel channel) {
            MyLog.D(TAG, "onDataChannel label()=" + channel.label());

            audioDataChannel = channel;
            audioDataChannel.registerObserver(audioDataObserver);
        }

        @Override
        public void onRenegotiationNeeded() {

        }

        @Override
        public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
            MyLog.E(TAG, "onAddTrack");
        }

        @Override
        public void onTrack(RtpTransceiver transceiver) {
            MyLog.E(TAG, "onTrack");
        }

        @Override
        public void onConnectionChange(PeerConnection.PeerConnectionState newState) {
            MyLog.D(TAG, "onConnectionChange PeerConnectionState=" + newState);

            if(newState == PeerConnection.PeerConnectionState.CONNECTED) {
                dataCallback.onDataRecv(peerId, "New connect at " + peerId+" isP2P="+isP2P);
                socketServer.start();
                dataCallback.onConnectChange(PeerConnectHelper.this);

            }else if(newState == PeerConnection.PeerConnectionState.DISCONNECTED){
                //dataCallback.onDataRecv(peerId,peerId + " disconnect");
                dataCallback.onConnectChange(PeerConnectHelper.this);

            }else if(newState == PeerConnection.PeerConnectionState.FAILED){
                //dataCallback.onDataRecv(peerId,peerId + " fail");
                socketServer.stop();
                dataCallback.onDisConnect(PeerConnectHelper.this, isCreateOffer);
            }
        }
    };

    private long lastAudioMessage = 0;

    private DataChannel.Observer audioDataObserver = new DataChannel.Observer() {
        @Override
        public void onBufferedAmountChange(long l) {
        }

        @Override
        public void onStateChange() {
            MyLog.D(TAG, "audioDataChannel onStateChange=" + audioDataChannel.state());
            ByteBuffer buffer = ByteBuffer.wrap("DataChannel Hello".getBytes());
            audioDataChannel.send(new DataChannel.Buffer(buffer, true));
        }

        @Override
        public void onMessage(DataChannel.Buffer buffer) {
            try {
                if (buffer.binary) {
                    long delta = System.currentTimeMillis() - lastAudioMessage;
                    if (delta > 200) {
                        MyLog.E(TAG, "audioDataChannel binary data, length=" + buffer.data.remaining() + ",delta=" + (System.currentTimeMillis() - lastAudioMessage) + "ms");
                    } else {
                        MyLog.D(TAG, "audioDataChannel binary data, length=" + buffer.data.remaining() + ",delta=" + (System.currentTimeMillis() - lastAudioMessage) + "ms");
                    }

                    lastAudioMessage = System.currentTimeMillis();
                } else {
                    ByteBuffer byteBuffer = buffer.data;
                    byte[] buf = new byte[byteBuffer.remaining()];
                    byteBuffer.get(buf);

                    dataCallback.onDataRecv(peerId, new String(buf));
                }
            }catch (Exception e){
                e.printStackTrace();
            }

        }
    };

    public void sendDataChannel(String data) {
        if (audioDataChannel != null && audioDataChannel.state() == DataChannel.State.OPEN) {

            while (true) {
                long amount = audioDataChannel.bufferedAmount();

                //MyLog.D(TAG, "sendDataChannel amount="+amount);

                if(amount == 0){
                    ByteBuffer byteBuffer = ByteBuffer.wrap(data.getBytes());
                    DataChannel.Buffer buffer = new DataChannel.Buffer(byteBuffer, false);

                    audioDataChannel.send(buffer);
                    break;
                }
            }
        }
    }

    public void startP2PConnect() {
        MyLog.I(TAG, "---------->startP2PConnect<-----------");

        isStartP2p = true;

        if (iceCandidateList.size() > 0) {
            for (IceCandidate iceCandidate : iceCandidateList) {
                peerConnection.addIceCandidate(iceCandidate);
                MyLog.I(TAG, "addCacheIceCandidate iceCandidate:" + iceCandidate.sdp);
            }

            iceCandidateList.clear();
        }
    }

    public void addIce(SpdJson spdJson) {
        IceCandidate iceCandidate = new IceCandidate(spdJson.sdpMid, spdJson.sdpMLineIndex, spdJson.sdp);

        MyLog.I(TAG,"onRecv IceCandidate:"+spdJson.sdp);

        if (isStartP2p) {
            peerConnection.addIceCandidate(iceCandidate);
        } else {
            iceCandidateList.add(iceCandidate);
        }
    }

    public void removeIce(SpdJson spdJson){
        MyLog.E(TAG,"removeIce:"+spdJson.sdp);

        IceCandidate iceCandidate = new IceCandidate(spdJson.sdpMid, spdJson.sdpMLineIndex, spdJson.sdp);
        peerConnection.removeIceCandidates(new IceCandidate[]{iceCandidate});
    }

    public boolean isClosed(){
        boolean ret = false;

        if(peerConnection == null) {

        }else if(peerConnection.connectionState() == PeerConnection.PeerConnectionState.CLOSED
                || peerConnection.connectionState() == PeerConnection.PeerConnectionState.FAILED){
            ret = true;
        }

        return ret;
    }

    public void writeDateToClient(DataCmd dataCmd){
        socketServer.writeDataToPort(dataCmd);
    }

    public void closeClient(DataCmd dataCmd){
        socketServer.closeClient(dataCmd);
    }

    public void writeDateToServer(DataCmd dataCmd){
        SocketConnect socketConnect = connectMap.get(dataCmd.fileId);
        if(socketConnect == null){
            socketConnect = new SocketConnect(this, dataCmd.fileId, () -> {
                connectMap.remove(dataCmd.fileId);
            });
            connectMap.put(dataCmd.fileId, socketConnect);
        }

        socketConnect.writeDate(dataCmd);
    }

    public void closeConnect(DataCmd dataCmd){
        SocketConnect socketConnect = connectMap.get(dataCmd.fileId);
        if(socketConnect != null){
            socketConnect.close();
        }
    }
}
